[TDR Generic表][C++ SDK]TDR Blob内嵌Protobuf部分字段读写
混合字段路径的综合读写示例、路径语法及限制见《TDR Blob 内嵌 Protobuf 部分字段读写》。本文介绍 C++ 的
TdrPbFieldGroup、SetAllTdrPbFieldNames,以及完整的 Service API 请求与响应示例。
1. 接口说明
TDR 表的 Blob 字段(char 数组 + refer 长度字段)里如果保存的是 Protobuf 序列化数据,
可以只读取或只更新 Blob 中指定的 PB 字段,而不必整段回读、改完再整段回写。
三个命令对应三个命令字:
| 命令 | 命令字 | 值 | 说明 |
|---|---|---|---|
| FieldGet | TCAPLUS_API_PB_FIELD_GET_REQ |
0x0067 | 读取 Blob 中指定的 PB 字段 |
| FieldSet | TCAPLUS_API_PB_FIELD_SET_REQ |
0x0069 | 更新 Blob 中指定的 PB 字段,记录不存在会报错,不会自动插入 |
| BatchFieldGet | TCAPLUS_API_PB_BATCH_FIELD_GET_REQ |
0x0075 | 多个主键共用一组字段路径批量读取 |
Blob 中 Protobuf 字段的自增(TCAPLUS_API_PB_FIELD_INCREASE_REQ)在 TDR 表上不支持。
本特性的字段名适配层是可选头文件 tcaplus_tdr_pb_fields.h,只替换这一行:
request->SetFieldNames(field_names, field_count);
其余请求和响应流程保持 Service API 原有用法不变。不 include 该头文件的 TDR 用户不会被强制依赖 protobuf。
2. 版本要求
- Service API 3.55.0 及以上;
- 服务端需要支持该特性。未适配的服务端在这三个命令上会直接返回找不到 PB 描述的错误,使用前请先确认服务端版本。
3. 准备工作
参见准备工作文档,完成使用该接口前的准备工作,并创建 TDR Generic 表。
本特性要求表里有一个 Blob 字段 + 它的 refer 长度字段。下面以 RoleSummaryTable 为例:
<struct name="Col" version="1">
<entry name="len" type="uint" defaultvalue="0" desc="bin 的有效长度" />
<entry name="bin" type="char" count="1024" refer="len" desc="PB 数据" />
</struct>
<struct name="RoleSummaryTable" version="1" primarykey="id" splittablekey="id">
<entry name="id" type="int64" desc="角色ID" />
<entry name="del" type="int" desc="删除标记" />
<entry name="col1" type="Col" desc="保存 ProfileBaseData 的 Blob" />
</struct>
col1.bin 是保存 PB 的 Blob 字段,col1.len 是它的 refer 长度字段。
注意区分两套名字:字段路径里用 TDR 表定义的字段名(col1.bin、col1.len、del),
C++ 代码里用 tdr 生成的成员名(stCol1.szBin、stCol1.dwLen、iDel、llId)。
Blob 里的 PB 定义示例(ProfileBaseData,不是 Tcaplus PB 表,只是普通 proto,不需要 tcaplus 的 proto 选项):
syntax = "proto3";
package tcaplus_demo;
message ProfileBaseData {
uint32 level = 5;
string plat_name = 6;
repeated uint32 equipped_tag_list = 16;
map<int32, BanInfo> ban_info_map = 22;
map<string, string> settings = 23;
}
message BanInfo { string info = 1; int32 duration = 2; }
完整示例见 examples/tcaplus/C++_tdr2.0_tdr_pb_fields:
| 内容 | 路径 |
|---|---|
| 表定义 | examples/tcaplus/C++_tdr2.0_tdr_pb_fields/table_test.xml |
| PB 定义 | examples/tcaplus/C++_tdr2.0_tdr_pb_fields/profile_base_data.proto |
| 示例代码 | examples/tcaplus/C++_tdr2.0_tdr_pb_fields/main.cpp |
| 编译与运行 | examples/tcaplus/C++_tdr2.0_tdr_pb_fields/readme.txt |
示例里的三个函数分别对应下面 5.1、5.2、5.3,响应处理对应 5.4。
4. 字段路径
部分字段读写的核心是字段路径,服务端没有业务的 .proto,只认数字 tag 路径:
| PB 定义 | 业务写法 | 字段路径 |
|---|---|---|
uint32 level = 5 |
level |
5 |
string plat_name = 6 |
plat_name |
6 |
repeated uint32 equipped_tag_list = 16 |
equipped_tag_list |
16(proto3 默认 packed,整组读写) |
map<int32, BanInfo> ban_info_map = 22 |
ban_info_map[1001] |
22[1001] |
| 同上 | ban_info_map[1001].info |
22[1001].1 |
map<string, string> settings = 23 |
settings['region'] |
23['region'] |
加上 Blob 字段名前缀后得到最终路径:col1.bin.5、col1.bin.22[1001].1。
要点:
- 承载路径的一级字段和 refer 长度字段由 SDK 自动补齐,业务不需要把
col1.bin、col1.len写进字段名列表; - 原生 TDR 字段直接写字段名,例如
del; - 删除 map 元素在路径上加
POP前缀(POP col1.bin.22[1001]); - 一条路径最多一层容器元素访问;
packed的标量数组只能整组读写,不支持下标。
字段名到 tag 路径的转换用 tcaplus_tdr_pb_fields.h 里的适配层:
#include "tcaplus_tdr_pb_fields.h"
const char* col1_fields[] = {
"level",
"plat_name",
"ban_info_map[1001]"
};
const TcaplusService::TdrPbFieldGroup field_groups[] = {
TcaplusService::TdrPbFieldGroup(
"col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields),
TcaplusService::TdrPbFieldGroup("del") // 原生 TDR 字段,原样透传
};
ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
不用适配层也可以,直接写数字 tag 路径:
const char* field_names[] = {"col1.bin.5", "col1.bin.6", "del"};
ret = request->SetFieldNames(field_names, 3);
4.1 TdrPbFieldGroup
表示一个原生 TDR 字段,或者同一个 Blob 中的一组 PB 字段。
// 原生 TDR 字段,路径不进入 PB 解析器,原样传给 SetFieldNames()
explicit TdrPbFieldGroup(const char* tdr_field_path);
// 显式的"数组指针 + 数量"形式。descriptor 为 NULL 表示调用方只提供数字 tag
TdrPbFieldGroup(
const char* tdr_blob_prefix,
const ::google::protobuf::Descriptor* descriptor,
const char* const pb_field_names[],
unsigned pb_field_count);
// 普通数组形式,自动推导字段数量
template <size_t N>
TdrPbFieldGroup(
const char* tdr_blob_prefix,
const ::google::protobuf::Descriptor* descriptor,
const char* const (&pb_field_names)[N]);
4.2 SetAllTdrPbFieldNames()
int32_t SetAllTdrPbFieldNames(
TcaplusServiceRequest* request,
const TdrPbFieldGroup field_groups[],
unsigned field_group_count);
template <size_t N>
int32_t SetAllTdrPbFieldNames(
TcaplusServiceRequest* request,
const TdrPbFieldGroup (&field_groups)[N]);
数组模板只是省去手写 sizeof(array) / sizeof(array[0])。适配层先把全部结果保存到临时容器,
确认所有分组都转换成功后,才调用一次 request->SetFieldNames(),本地转换失败不会给 request 留下半组字段。
5. 示例代码
Blob 里必须是合法的 PB 编码,普通写入的裸字符串服务端解析不了,先整段写一份完整 PB。
示例代码见 examples/tcaplus/C++_tdr2.0_tdr_pb_fields/main.cpp,对应函数为
SendProfileFieldSet、SendProfileFieldGet、SendProfileBatchFieldGet 和
HandleProfileFieldResponse,入口在 main() 中。
5.1 FieldSet(部分更新)
int32_t SendProfileFieldSet(TcaplusService::TcaplusServer& server, int64_t role_id)
{
const char kTableName[] = "RoleSummaryTable";
// 1. 创建并初始化原生 Service API request
TcaplusService::TcaplusServiceRequest* request = server.GetRequest(kTableName);
if (request == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
int32_t ret = request->Init(TcaplusService::TCAPLUS_API_PB_FIELD_SET_REQ);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
// 2. 业务构造增量 PB,并直接序列化到 TDR Blob
tcaplus_demo::ProfileBaseData delta;
delta.set_level(100);
delta.set_plat_name("plat_x");
(*delta.mutable_ban_info_map())[1001].set_info("ban info");
ROLESUMMARYTABLE row;
memset(&row, 0, sizeof(row));
row.llId = role_id; // tdr 生成的成员名,不是 xml 里的 id
const size_t encoded_size = delta.ByteSizeLong();
if (encoded_size > sizeof(row.stCol1.szBin))
{
return TcapErrCode::API_ERR_OVER_MAX_FIELD_VALUE_LEN;
}
if (!delta.SerializePartialToArray(row.stCol1.szBin, static_cast<int>(encoded_size)))
{
return TcapErrCode::API_ERR_PACK_MESSAGE;
}
row.stCol1.dwLen = static_cast<uint32_t>(encoded_size);
// 3. 继续使用原生 record;TDR key、Blob 和其他字段都由业务控制
TcaplusService::TcaplusServiceRecord* record = request->AddRecord();
if (record == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
ret = record->SetData(&row, sizeof(row));
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
// 4. 唯一被适配的步骤:按 PB 字段名设置部分字段集合
const char* col1_fields[] = {
"level",
"plat_name",
"ban_info_map[1001]"
};
const TcaplusService::TdrPbFieldGroup field_groups[] = {
TcaplusService::TdrPbFieldGroup(
"col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields)
};
ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
return server.SendRequest(request);
}
SetAllTdrPbFieldNames() 最终设置的是 col1.bin.5、col1.bin.6 和 col1.bin.22[1001],
只有这三个路径允许被 FieldSet 修改,Blob 中其余字段服务端原样保留。
5.2 FieldGet(部分读取,同时读原生 TDR 字段)
int32_t SendProfileFieldGet(TcaplusService::TcaplusServer& server, int64_t role_id)
{
const char kTableName[] = "RoleSummaryTable";
TcaplusService::TcaplusServiceRequest* request = server.GetRequest(kTableName);
if (request == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
int32_t ret = request->Init(TcaplusService::TCAPLUS_API_PB_FIELD_GET_REQ);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
ROLESUMMARYTABLE row;
memset(&row, 0, sizeof(row));
row.llId = role_id;
TcaplusService::TcaplusServiceRecord* record = request->AddRecord();
if (record == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
ret = record->SetData(&row, sizeof(row));
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
const char* col1_fields[] = {
"level",
"plat_name",
"ban_info_map[1001].info",
"settings['region']"
};
const TcaplusService::TdrPbFieldGroup field_groups[] = {
TcaplusService::TdrPbFieldGroup(
"col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields),
TcaplusService::TdrPbFieldGroup("del")
};
ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
return server.SendRequest(request);
}
del 不经过 PB 解析,和四个 PB 路径一起传入同一次 SetFieldNames()。
5.3 BatchFieldGet(批量读取)
int32_t SendProfileBatchFieldGet(
TcaplusService::TcaplusServer& server,
const int64_t role_ids[],
unsigned role_count)
{
const char kTableName[] = "RoleSummaryTable";
TcaplusService::TcaplusServiceRequest* request = server.GetRequest(kTableName);
if (request == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
int32_t ret = request->Init(TcaplusService::TCAPLUS_API_PB_BATCH_FIELD_GET_REQ);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
for (unsigned i = 0; i < role_count; ++i)
{
ROLESUMMARYTABLE row;
memset(&row, 0, sizeof(row));
row.llId = role_ids[i];
TcaplusService::TcaplusServiceRecord* record = request->AddRecord();
if (record == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
ret = record->SetData(&row, sizeof(row));
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
}
// 所有记录共用同一组字段路径,只设置一次
const char* col1_fields[] = {"level", "plat_name"};
const TcaplusService::TdrPbFieldGroup field_groups[] = {
TcaplusService::TdrPbFieldGroup(
"col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields)
};
ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
return server.SendRequest(request);
}
单次最多 1024 条主键。
5.4 响应处理
响应端不使用适配层,直接读取 TDR,再解析部分 PB:
int HandleProfileFieldResponse(TcaplusService::TcaplusServiceResponse* response)
{
if (response == NULL)
{
return TcapErrCode::API_ERR_PARAMETER_INVALID;
}
int32_t ret = response->GetResult();
if (ret != TcapErrCode::GEN_ERR_SUC)
{
return ret;
}
int32_t first_error = TcapErrCode::GEN_ERR_SUC;
const int record_count = response->GetRecordCount();
for (int i = 0; i < record_count; ++i)
{
const TcaplusService::TcaplusServiceRecord* record = NULL;
ret = response->FetchRecord(record);
if (ret != TcapErrCode::GEN_ERR_SUC)
{
if (first_error == TcapErrCode::GEN_ERR_SUC)
{
first_error = ret;
}
continue;
}
ROLESUMMARYTABLE row;
memset(&row, 0, sizeof(row));
ret = record->GetData(&row, sizeof(row));
if (ret != TcapErrCode::GEN_ERR_SUC)
{
if (first_error == TcapErrCode::GEN_ERR_SUC)
{
first_error = ret;
}
continue;
}
if (row.stCol1.dwLen > sizeof(row.stCol1.szBin))
{
if (first_error == TcapErrCode::GEN_ERR_SUC)
{
first_error = TcapErrCode::API_ERR_OVER_MAX_FIELD_VALUE_LEN;
}
continue;
}
tcaplus_demo::ProfileBaseData profile;
if (!profile.ParsePartialFromArray(row.stCol1.szBin, static_cast<int>(row.stCol1.dwLen)))
{
if (first_error == TcapErrCode::GEN_ERR_SUC)
{
first_error = TcapErrCode::API_ERR_UNPACK_MESSAGE;
}
continue;
}
// profile 只包含本次请求返回的部分字段,不能当作完整记录使用
ConsumeProfile(row.llId, row.iDel, profile);
}
return first_error;
}
6. 适配层错误码
本地检查(SetAllTdrPbFieldNames() 返回值):
| 场景 | 返回值 |
|---|---|
| request、数组、前缀或字段路径参数非法 | API_ERR_PARAMETER_INVALID |
| descriptor 中不存在字段名或 tag | API_ERR_FIELD_NOT_EXSIST |
| selector、map、repeated 或嵌套 message 类型不匹配 | API_ERR_FIELD_TYPE_NOT_MATCH |
| request 未初始化、字段数过多、完整路径过长 | 原样返回 SetFieldNames() 的错误码 |
发送后的结果看 response->GetResult()。常见服务端错误码:
| 错误码 | 含义 |
|---|---|
TXHDB_ERR_RECORD_NOT_EXIST |
记录不存在 |
COMMON_ERR_ELEMENT_NOT_EXIST |
指定的 map key 或 repeated 下标不存在 |
COMMON_ERR_CONDITION_NOT_MATCHED |
条件不成立 |
SVR_ERR_FAIL_INVALID_VERSION |
版本校验失败 |
详见错误码含义和处理方法。
7. 注意事项
- 只支持 Generic 表,List 表只能由服务端拒绝,SDK 本地拿不到表类型。
- Blob 里必须是当前业务 descriptor 对应的 PB 编码,历史数据的 schema 兼容性由业务保证。
- 返回的
col1.len是本次部分 PB 的长度,不是记录中完整 Blob 的长度; 解析出的 PB 对象只包含本次请求的字段,不能直接用于整段 Blob 覆盖。 - SDK 不做 PB 编解码,增量 PB 的构造、Blob 的写入、响应 Blob 的解析都由业务负责,
适配层只解决
PB field name -> field number的映射。 - 路径里的一级字段名取 TDR 表定义中的字段名,不是 C++ 结构体字段名。
- 字段名使用
.proto中的原始 field name,不使用 JSON name。 - 头文件依赖完整 protobuf descriptor API,传入
NULL只表示本次按数字 tag 转换; 不 include 该头文件的 TDR 用户不会被引入 protobuf 依赖。 - 嵌套结构体里的 Blob(如本例的
col1.bin)也支持,补齐的是嵌套结构体字段本身。